//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//    NotificationConditionHelper.cs
//
// Abstract:
//    
//    Helper class to generate descriptions from NotificationConditions.
//
//--

using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;

namespace Microsoft.EnhancedReminders.OutlookAddIn
{
    /// <summary>
    /// Generates descriptive text for each condition type.
    /// </summary>
    /// <remarks>
    /// Ideally each NotificationCondition type would implement its own "GenerateDescription()"
    /// overload.  However, as these generated strings need to be globalized, it is best to
    /// do this here within the UI code rather than in Common (where NotificationCondition is
    /// defined).
    /// </remarks>
    internal static class NotificationConditionHelper
    {
        /// <summary>
        /// Returns the first selected condition for the given rule or null if none was found.
        /// </summary>
        /// <param name="rule">The notification rule.</param>
        /// <returns>The selected condition or null if not found.</returns>
        internal static NotificationCondition FindFirstSelectedCondition(NotificationRuleItem rule)
        {
            if (rule == null)
            {
                return null;
            }

            Debug.Assert(rule.Conditions != null, "rule.Conditions");
            Debug.Assert(rule.Conditions.Count > 0, "Must be at least one condition");

            NotificationCondition selectedCondition = null;

            foreach (NotificationCondition condition in rule.Conditions)
            {
                if (condition.IsSelected)
                {
                    selectedCondition = condition;
                    break;
                }
            }

            return selectedCondition;
        }

        /// <summary>
        /// Generate the description for the selected conditions of the given rule.
        /// </summary>
        /// <param name="rule">The rule for which to generate the condition description.</param>
        /// <param name="maxLength">The maximum length of the description string.</param>
        internal static string GenerateConditionDescription(NotificationRuleItem rule, int maxLength)
        {
            Debug.Assert(rule != null, "rule");

            StringBuilder description = new StringBuilder();

            foreach (NotificationCondition condition in rule.Conditions)
            {
                if (condition.IsSelected)
                {
                    if (description.Length > 0) // Append " and " if not the first condition.
                    {
                        description.Append(Properties.Resources.RulesDialogAnd);
                    }

                    description.Append(GenerateConditionDescription(condition));
                }
            }

            return WPFHelpers.TruncateString(description.ToString(), maxLength);
        }

        /// <summary>
        /// Generate the description for the given condition and truncate the description string if 
        /// it exceeds the specified maximum size.
        /// </summary>
        /// <param name="condition">The condition for which to generate the description.</param>
        internal static string GenerateConditionDescription(NotificationCondition condition)
        {
            Debug.Assert(condition != null, "condition != null");

            string description = null;

            NotificationTimeCondition timeCondition = condition as NotificationTimeCondition;
            NotificationPeopleCondition peopleCondition = condition as NotificationPeopleCondition;
            NotificationStatusCondition statusCcondition = condition as NotificationStatusCondition;

            if (timeCondition != null)
            {
                description = GenerateTimeConditionDescription(timeCondition);
            }
            else if (peopleCondition != null)
            {
                description = GeneratePeopleConditionDescription(peopleCondition);
            }
            else if (statusCcondition != null)
            {
                description = GenerateStatusConditionDescription(statusCcondition);
            }
            else
            {
                Debug.Fail("Unexpected condition type");
            }

            Debug.Assert(!String.IsNullOrEmpty(description), "description");

            return description;
        }

        /// <summary>
        /// Generate the description for the given time condition.  This converts a bitmap of 1/2 hour intervals 
        /// into a descripion of the start and end times of each block of "on" bits (e.g. "After 2:00 PM and 
        /// before 7:30 PM on Tuesday").
        /// </summary>
        internal static string GenerateTimeConditionDescription(NotificationTimeCondition timeCondition)
        {
            Debug.Assert(IsValidTimeCondition(timeCondition), "Time description is null or no cells have been selected");
            Debug.Assert(timeCondition.TimeArray.Length == NotificationTimeCondition.IntervalsPerDay * NotificationTimeCondition.NumberOfDays, "timeCondition.TimeArray.Length");
            Debug.Assert(NotificationTimeCondition.IntervalsPerDay > 0, "Must be at least one interval per day");
            Debug.Assert(/*Minutes Per Day*/1440 % NotificationTimeCondition.IntervalsPerDay == 0, "A day must have an integral number of intervals");

            const int MinsPerInterval = /*Minutes Per Day*/1440 / NotificationTimeCondition.IntervalsPerDay;

            StringBuilder description = new StringBuilder();

            // September 2nd, 2007 starts on a Sunday so use this as a reference point for the start of the week.
            DateTime startOfWeek = new DateTime(/*year*/2007, /*month*/9, /*day*/2);

            DateTime start = startOfWeek;

            CultureInfo cultureInfo = Thread.CurrentThread.CurrentUICulture;
            DateTimeFormatInfo dateTimeFormatInfo = DateTimeFormatInfo.GetInstance(cultureInfo);

            // Iterate through the times bitmap to pick out the start/end time blocks.
            for (int i = 0, startDayIndex = -1; i < timeCondition.TimeArray.Length; i++)
            {
                // Detect the start of a new time interval (bits transition from 0->1).
                if (timeCondition.TimeArray[i] && (i == 0 || !timeCondition.TimeArray[i - 1]))
                {
                    if (startDayIndex >= 0) // This was not the first time interval so add a separator.
                    {
                        description.Append(Properties.Resources.RulesDialogDescriptionSeparator);
                    }

                    // Index i starts a new time interval.
                    start = startOfWeek.AddMinutes(MinsPerInterval * i);
                    startDayIndex = i / NotificationTimeCondition.IntervalsPerDay;
                    description.Append(Properties.Resources.RulesDialogTimeDescriptionAfter);
                    description.Append(GetLocalizedDateTimeString(start)); // Appends the short time description (e.g. 2:30 AM)
                }

                // Detect the end of a time block (transition from 1->0 or the very last bit is 1.  
                if ((!timeCondition.TimeArray[i] && i > 0 && timeCondition.TimeArray[i - 1]) ||
                     (i == timeCondition.TimeArray.Length - 1 && timeCondition.TimeArray[i]))
                {
                    // Index from start of next block so that end time spans right to the end of the last interval.
                    // Note that index i will be just past the end of the block (in the 
                    // next interval) unless i is the last bit in the array.
                    int endIndex = i == timeCondition.TimeArray.Length - 1 ? i + 1 : i;
                    DateTime end = startOfWeek.AddMinutes(MinsPerInterval * endIndex);

                    // Check if block ends in a different day.
                    if (start.DayOfWeek != end.DayOfWeek)
                    {
                        description.Append(Properties.Resources.RulesDialogTimeDescriptionOn);

                        Debug.Assert(startDayIndex < dateTimeFormatInfo.DayNames.Length,
                                     "startDayIndex exceeds bounds of the day names array");

                        description.Append(dateTimeFormatInfo.DayNames[startDayIndex]);
                    }

                    description.Append(Properties.Resources.RulesDialogTimeDescriptionBefore);
                    description.Append(GetLocalizedDateTimeString(end));
                    description.Append(Properties.Resources.RulesDialogTimeDescriptionOn);

                    // If the last bit is on then end time will be midnight on Sunday (day 0).
                    int endDayIndex = i == timeCondition.TimeArray.Length - 1 ? 0 : i;

                    Debug.Assert(endDayIndex / NotificationTimeCondition.IntervalsPerDay < dateTimeFormatInfo.DayNames.Length,
                                 "endDayIndex / intervalsPerDay exceeds bounds of the day names array");

                    description.Append(dateTimeFormatInfo.DayNames[endDayIndex / NotificationTimeCondition.IntervalsPerDay]);
                }
            }

            return description.ToString();
        }

        /// <summary>
        /// Generates the description for a "people" condition instance.
        /// </summary>
        internal static string GeneratePeopleConditionDescription(NotificationPeopleCondition peopleCondition)
        {
            Debug.Assert(IsValidPeopleCondition(peopleCondition), "PeopleCondition is null or no names/aliases have been specified.");

            StringBuilder people = new StringBuilder(Properties.Resources.RulesDialogPeopleFrom);

            foreach (string name in peopleCondition.Names)
            {
                people.Append(name);
                if (peopleCondition.Names.IndexOf(name) < peopleCondition.Names.Count - 1)
                {
                    people.Append(Properties.Resources.RulesDialogDescriptionSeparator); // " or "
                }
            }

            return people.ToString();
        }

        /// <summary>
        /// Generates a description for the given status condition.
        /// </summary>
        internal static string GenerateStatusConditionDescription(NotificationStatusCondition statusCondition)
        {
            Debug.Assert(IsValidStatusCondition(statusCondition), "Status condition is null or has not been specified");

            string statusText = Properties.Resources.RulesDialogStatusPrefix;

            int states = (int)statusCondition.Presence;

            // Test each state, appending its text if present, and remove the state from states.
            AddStatusText(PresenceStates.Available, ref states, ref statusText);
            AddStatusText(PresenceStates.Busy, ref states, ref statusText);
            AddStatusText(PresenceStates.InAMeeting, ref states, ref statusText);
            AddStatusText(PresenceStates.InACall, ref states, ref statusText);
            AddStatusText(PresenceStates.InAConference, ref states, ref statusText);
            AddStatusText(PresenceStates.DoNotDisturb, ref states, ref statusText);
            AddStatusText(PresenceStates.BeRightBack, ref states, ref statusText);
            AddStatusText(PresenceStates.Away, ref states, ref statusText);
            AddStatusText(PresenceStates.IdleOnline, ref states, ref statusText);
            AddStatusText(PresenceStates.IdleBusy, ref states, ref statusText);
            AddStatusText(PresenceStates.Offline, ref states, ref statusText);
            AddStatusText(PresenceStates.Unknown, ref states, ref statusText);

            // Verify all states have been tested.
            Debug.Assert(states == 0, "An unexpected presence state was found - was a new state added?");

            return statusText;
        }

        /// <summary>
        /// Helper method to append the description for a given state if its in the list of states.
        /// </summary>
        /// <param name="currentState">The state to test.</param>
        /// <param name="stateList">The list of states.</param>
        /// <param name="text">The description to append state's text to.</param>
        private static void AddStatusText(PresenceStates currentState, ref int stateList, ref string text)
        {
            if ((stateList & (int)currentState) == 0)
            {
                return;
            }

            string currentStateText = null;

            switch (currentState)
            {
                case PresenceStates.Available:
                    currentStateText = Properties.Resources.PresenceStateAvailable;
                    break;
                case PresenceStates.Busy:
                    currentStateText = Properties.Resources.PresenceStateBusy;
                    break;
                case PresenceStates.InAMeeting:
                    currentStateText = Properties.Resources.PresenceStateInAMeeting;
                    break;
                case PresenceStates.InACall:
                    currentStateText = Properties.Resources.PresenceStateInACall;
                    break;
                case PresenceStates.InAConference:
                    currentStateText = Properties.Resources.PresenceStateInAConference;
                    break;
                case PresenceStates.DoNotDisturb:
                    currentStateText = Properties.Resources.PresenceStateDoNotDisturb;
                    break;
                case PresenceStates.BeRightBack:
                    currentStateText = Properties.Resources.PresenceStateBeRightBack;
                    break;
                case PresenceStates.Away:
                    currentStateText = Properties.Resources.PresenceStateAway;
                    break;
                case PresenceStates.IdleOnline:
                    currentStateText = Properties.Resources.PresenceStateIdleOnline;
                    break;
                case PresenceStates.IdleBusy:
                    currentStateText = Properties.Resources.PresenceStateIdleBusy;
                    break;
                case PresenceStates.Offline:
                    currentStateText = Properties.Resources.PresenceStateOffline;
                    break;
                case PresenceStates.Unknown:
                    currentStateText = Properties.Resources.PresenceStateUnknown;
                    break;
                default:
                    Debug.Fail("Unexpected presence state - has the enum been updated?");
                    break;
            }

            text += "\"" + currentStateText + "\"";

            // Remove this state from the list of states.
            stateList ^= (int)currentState;

            if (stateList != 0) // Add separator " or " if more states remain.
            {
                text += Properties.Resources.RulesDialogDescriptionSeparator;
            }
        }

        /// <summary>
        /// Verifies that the data displayed on the conditions page is valid for a rule, i.e. 
        ///    - one or more conditions are selected 
        ///    - each selected condition is valid.
        ///    - rule name has been populated
        ///    - rule type is set to voicemail or calendar.
        /// </summary>
        /// <param name="rule">The notification rule to validate.</param>
        internal static bool VerifyConditions(NotificationRuleItem rule)
        {
            if (rule == null || // Rule can be null when UI is initialising the add/edit dialog.
                String.IsNullOrEmpty(rule.RuleName) ||
                !(rule.MailboxType == MailboxType.Voicemail || rule.MailboxType == MailboxType.CalendarItem))
            {
                return false;
            }

            bool foundSelectedCondition = false;

            foreach (NotificationCondition condition in rule.Conditions)
            {
                if (condition.IsSelected)
                {
                    if (!IsValidCondition(condition))
                    {
                        return false;
                    }
                    foundSelectedCondition = true;
                }
            }

            return foundSelectedCondition;
        }

        /// <summary>
        /// Tests if a condition is non-null and valid.
        /// </summary>
        /// <param name="condition">The condition to test.</param>
        internal static bool IsValidCondition(NotificationCondition condition)
        {
            return IsValidTimeCondition(condition as NotificationTimeCondition) ||
                   IsValidPeopleCondition(condition as NotificationPeopleCondition) ||
                   IsValidStatusCondition(condition as NotificationStatusCondition);
        }

        /// <summary>
        /// Tests if a time condition is non-null and valid.
        /// </summary>
        /// <param name="timeCondition">The time condition to test.</param>
        internal static bool IsValidTimeCondition(NotificationTimeCondition timeCondition)
        {
            Debug.Assert(timeCondition == null || timeCondition.TimeArray != null,
                         "TimeArray must be populated for a NotificationTimeCondition instance");

            if (timeCondition != null)
            {
                // Need to iterate through the array as TimeArray.Equals(new BitArray(TimeArray.Length))
                // only compares if the instances are the same.
                foreach (bool cell in timeCondition.TimeArray)
                {
                    if (cell)
                    {
                        return true; // At least one cell in the time grid is selected.
                    }
                }
            }

            return false; // timeCondition either null or all cells are 0.
        }

        /// <summary>
        /// Tests if a people condition is non-null and valid.
        /// </summary>
        /// <param name="peopleCondition">The people condition to test.</param>
        internal static bool IsValidPeopleCondition(NotificationPeopleCondition peopleCondition)
        {
            Debug.Assert(peopleCondition == null || peopleCondition.Names != null, 
                         "Names field must be populated for a NotificationPeopleCondition instance.");

            return peopleCondition != null && peopleCondition.Names.Count > 0;
        }

        /// <summary>
        /// Tests if a status condition is non-null and valid.
        /// </summary>
        /// <param name="statusCondition">The status condition to test.</param>
        internal static bool IsValidStatusCondition(NotificationStatusCondition statusCondition)
        {
            return statusCondition != null && statusCondition.Presence != PresenceStates.None;
        }

        /// <summary>
        /// Generates descriptive text for the action link.
        /// </summary>
        /// <param name="ruleType">The rule type,</param>
        /// <param name="notificationChannels">The notification channels.</param>
        /// <param name="joinNowChannels">The join now channels.</param>
        /// <param name="maxLength">Maximum length of the string at which to truncate.</param>
        internal static string GenerateActionDescription(MailboxType ruleType, NotificationChannelListAbsoluteTime notificationChannels, NotificationChannelListLeadTime joinNowChannels, int maxLength)
        {
            Debug.Assert(notificationChannels != null, "notificationChannels");
            Debug.Assert(joinNowChannels != null, "joinNowChannels");

            StringBuilder description = new StringBuilder();

            // The voicemail channels are stored as a NotificationChannelListAbsoluteTime which causes "IsEnabled" 
            // to fail as the absolute time is not set.  Create a NotificationChannelList to circumvent this. 
            NotificationChannelList voicemailChannels = new NotificationChannelList(notificationChannels.Channels);

            if ((ruleType == MailboxType.Voicemail && voicemailChannels.IsEnabled) ||
                (ruleType == MailboxType.CalendarItem && notificationChannels.IsEnabled))
            {
                if (ruleType == MailboxType.CalendarItem)
                {
                    description.Append(Properties.Resources.RulesDialogActionCalendarPrefix);
                    description.Append(GetLocalizedDateTimeString(notificationChannels.AbsoluteTime));
                    description.Append(' ');
                }

                IterateChannels(notificationChannels, description);               
            }

            if (joinNowChannels.IsEnabled)
            {
                Debug.Assert(ruleType == MailboxType.CalendarItem);

                if (description.Length > 0) // Found a notification channel
                {
                    description.Append(Properties.Resources.RulesDialogAnd);
                }               
                description.Append(Properties.Resources.RulesDialogActionAutoJoinPrefix);

                IterateChannels(joinNowChannels, description);
            }

            if (description.Length == 0)
            {
                description.Append(Properties.Resources.RulesDialogActionInvalid);
            }

            return WPFHelpers.TruncateString(description.ToString(), maxLength);
        }

        /// <summary>
        /// Iterate through a channel list to append any enabled channels to the action description.
        /// </summary>
        /// <param name="channels">The channel list.</param>
        /// <param name="description">The description to append to.</param>
        private static void IterateChannels(NotificationChannelList channels, StringBuilder description)
        {
            bool firstChannel = true;

            foreach (NotificationChannel channel in channels.Channels)
            {
                if (!channel.IsEnabled)
                {
                    continue;
                }

                if (!firstChannel)
                {
                    description.Append(Properties.Resources.RulesDialogActionThen);
                }

                description.Append(GenerateAction(channel, channels.GetType() == typeof(NotificationChannelListLeadTime)));

                firstChannel = false;
            }
        }

        /// <summary>
        /// Create an action description for a single channel.
        /// </summary>
        /// <param name="channel">The channel to describe.</param>
        /// <param name="isJoinNow">True if a "join now" channel; false otherwise.</param>
        private static string GenerateAction(NotificationChannel channel, bool isJoinNow)
        {
            string action = null;

            if (!isJoinNow)
            {
                switch (channel.NotificationChannelType)
                {
                    case NotificationChannelType.Communicator:
                        action = Properties.Resources.RulesDialogActionSendIM;
                        break;

                    case NotificationChannelType.Phone:
                        action = Properties.Resources.RulesDialogActionCall;
                        break;

                    case NotificationChannelType.SMS:
                        action = Properties.Resources.RulesDialogActionSendSms;
                        break;

                    default:
                        break;
                }

                Debug.Assert(!String.IsNullOrEmpty(action), "Failed to populate action string - either empty resource or unexpected channel type");
                
                action += ' ';
            }

            action += '\"' + channel.UserDefinedName + '\"';

            return action;
        }

        /// <summary>
        /// Helper method to generate the localized short date time string (e.g. "1:30 PM").
        /// </summary>
        private static string GetLocalizedDateTimeString(DateTime dateTime)
        {
            CultureInfo cultureInfo = Thread.CurrentThread.CurrentUICulture;

            return dateTime.ToString("t", cultureInfo);
        }
    }
}
